Visualize preferential data using ternary plots in 2D and higher dimensions
ABC News shows interesting plot on 2025 House of Rep’s distribution of first preference
But some challenges with this plot:
=> prefviz creates a uniform way of visualizing ternary plot of any dimensions
First Preference Distribution
Hover to see electorate names and exact vote percentages.
Full Preference Flow
Track how votes move between parties as candidates are eliminated.
Explore with map of electorate
Link the ternary plot with a map of electorates. Selecting a point in the ternary plot highlights the corresponding electorate on the map, and vice versa.
For elections with 4+ significant parties, we integrate with tourr and detourr for dynamic rotations through preference space.
prefviz functions| Step | Data Transformation | Construct Ternary Components | Visualization |
|---|---|---|---|
| What it does | Convert raw ballot data to aggregated compositional percentages | Build geometric infrastructure (vertices, edges, coordinates) | Interactive 2D or high-dimensional plots |
| Key functions | dop_irv() – for raw ballot datadop_transform() – reshape aggregated data |
ternable() – create ternable objectget_tern_*() - Getter functions for transforming components to appropriate shapes |
ggplot2 + plotly (2D)tourr + detourr (high-D) |
| Input | Raw ballots or aggregated preferences | Standardized compositional data | Ternary components from ternable object |
| Output | Clean, standardized format | Geometric objects ready to plot | Interactive, explorable visualization |
Raw data is from AEC’s 2025 House of Representatives distribution of preferences.
Instead of raw ballot data, AEC provides aggregated percentage, with each row representing the preference for a party in a round of voting (CountNumber), at a specific electorate (DivisionNm).
# A tibble: 5 × 15
StateAb DivisionID DivisionNm CountNumber BallotPosition CandidateID Surname
<chr> <dbl> <chr> <dbl> <dbl> <dbl> <chr>
1 ACT 318 Bean 0 1 41076 LAMERTON
2 ACT 318 Bean 0 2 41682 PRICE
3 ACT 318 Bean 0 3 41436 CARTER
4 ACT 318 Bean 0 4 40676 SMITH
5 ACT 318 Bean 1 1 41076 LAMERTON
GivenNm PartyAb PartyNm Elected HistoricElected
<chr> <chr> <chr> <chr> <chr>
1 David LP Liberal N N
2 Jessie IND Independent N N
3 Sam GRN Australian Greens N N
4 David ALP Australian Labor Party Y Y
5 David LP Liberal N N
CalculationType CalculationValue Party
<chr> <dbl> <chr>
1 Preference Percent 23.0 LNP
2 Preference Percent 26.4 Other
3 Preference Percent 9.5 Other
4 Preference Percent 41.0 ALP
5 Preference Percent 23.3 LNP
Transformed data with 3 columns representing the composition of each party in each electorate, summing to 1.
pref25_2d <- dop_transform(
data = pref25_2d,
key_cols = c(DivisionNm, CountNumber),
value_col = CalculationValue,
item_col = Party,
winner_col = Elected,
winner_identifier = "Y"
)
pref25_2d# A tibble: 976 × 6
DivisionNm CountNumber ALP LNP Other Winner
<chr> <dbl> <dbl> <dbl> <dbl> <chr>
1 Adelaide 0 0.465 0.242 0.294 ALP
2 Adelaide 1 0.467 0.242 0.291 ALP
3 Adelaide 2 0.476 0.244 0.279 ALP
4 Adelaide 3 0.483 0.249 0.268 ALP
5 Adelaide 4 0.493 0.285 0.222 ALP
6 Adelaide 5 0.691 0.309 0 ALP
7 Aston 0 0.373 0.377 0.251 ALP
8 Aston 1 0.373 0.378 0.249 ALP
9 Aston 2 0.376 0.380 0.244 ALP
10 Aston 3 0.378 0.384 0.238 ALP
# ℹ 966 more rows
ternable() creates a ternable object, which is a S3 object that contains the data and metadata necessary for ternary plots, including the vertices, edges, labels of the simplex, and coordinates of all data points.
List of 6
$ data : tibble [976 × 6] (S3: tbl_df/tbl/data.frame)
..$ DivisionNm : chr [1:976] "Adelaide" "Adelaide" "Adelaide" "Adelaide" ...
..$ CountNumber: num [1:976] 0 1 2 3 4 5 0 1 2 3 ...
..$ ALP : num [1:976] 0.465 0.467 0.476 0.483 0.493 ...
..$ LNP : num [1:976] 0.242 0.242 0.244 0.249 0.285 ...
..$ Other : num [1:976] 0.294 0.291 0.279 0.268 0.222 ...
..$ Winner : chr [1:976] "ALP" "ALP" "ALP" "ALP" ...
$ ternary_coord : tibble [976 × 2] (S3: tbl_df/tbl/data.frame)
..$ x1: num [1:976] 0.158 0.159 0.164 0.165 0.147 ...
..$ x2: num [1:976] 0.0487 0.0522 0.0662 0.0801 0.1367 ...
$ data_edges : int [1:976, 1:2] 1 2 3 4 5 6 7 8 9 10 ...
..- attr(*, "dimnames")=List of 2
.. ..$ : NULL
.. ..$ : chr [1:2] "Var1" "Var2"
$ simplex_vertices: tibble [3 × 3] (S3: tbl_df/tbl/data.frame)
..$ x1 : num [1:3] 0.707 -0.707 0
..$ x2 : num [1:3] 0.408 0.408 -0.816
..$ labels: chr [1:3] "ALP" "LNP" "Other"
$ simplex_edges : int [1:6, 1:2] 2 3 1 3 1 2 1 1 2 2 ...
..- attr(*, "dimnames")=List of 2
.. ..$ : chr [1:6] "2" "3" "4" "6" ...
.. ..$ : chr [1:2] "Var1" "Var2"
$ vertex_labels : chr [1:3] "ALP" "LNP" "Other"
- attr(*, "class")= chr "ternable"
prefviz includes some ggplot2 extensions to make creating ternary plots easier. Output is compatible with plotly and ggiraph.
input_data <- get_tern_data(tern_2d, plot_type = "2D") |>
mutate(text = paste0(DivisionNm, "\n",
"ALP: ", round(ALP, 1), "%\n",
"LNP: ", round(LNP, 1), "%\n",
"Other: ", round(Other, 1), "%"))
p2d <- ggplot(input_data |> filter(CountNumber == 0), aes(x = x1, y = x2)) +
geom_ternary_cart() +
geom_ternary_region(
aes(fill = after_stat(vertex_labels)),
x1 = 1/3, x2 = 1/3, x3 = 1/3,
vertex_labels = tern_2d$vertex_labels,
alpha = 0.3, color = NA, show.legend = FALSE
) +
add_vertex_labels(tern_2d$simplex_vertices) +
geom_point(aes(color = Winner, text = text)) +
scale_fill_manual(
values = c("ALP" = "red", "LNP" = "blue", "Other" = "grey70")
) +
scale_color_manual(
values = c("ALP" = "red", "LNP" = "blue", "Other" = "grey70"),
name = "Elected Party"
) +
labs(title = "First preference in 2022 Australian Federal election")
plotly_ternary <- ggplotly(p2d, tooltip = "text", width = 600, height = 400)prefviz depends on tourr for high-dimensional visualization, and detourr for interactive tours.
Priorities
Potential avenues
detourr integration.